package ms.unicorn.reasoning.optimize;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

public class FDGraph implements Serializable {
	
	private static final long serialVersionUID = 1L;

	private List<FDNode> nodes;
	
	long timeInitialization, timePropagation;
	private List<FDNode> leaves;
	private Map<FDNode, Integer> ready;
	private FDNode dummyRoot;
	private boolean autoConnectToRoot = true;

	private String name;
	
	public FDGraph() {
		nodes = new ArrayList<FDNode>();
		leaves = new ArrayList<FDNode>();
	}
	
	public FDGraph(String name) {
		this();
		this.name = name;	
		dummyRoot = new FDNode(this, FDNode.FDNODE_REQUIREMENT, "dummyRoot");
		nodes.add(dummyRoot);
	}
	
	public void write(ObjectOutputStream out) throws IOException {
		out.writeUTF("hypergraph");
		out.writeUTF(name);
		out.writeLong(timePropagation);
		out.writeInt(nodes.size());
		for (FDNode node: nodes) {
			out.writeInt(node.getNodetype());
			out.writeUTF(node.getName());
		}
		out.writeUTF("relation");
		for (FDNode node: nodes) {
			out.writeInt(node.getParentNode().size());
			for (FDNode parent: node.getParentNode())
				out.writeInt(parent.getNodeID());
		}
	}
	
	public void read(ObjectInputStream in) throws IOException {
		assert "hypergraph".equals(in.readUTF());
		name = in.readUTF();
		timePropagation = in.readLong();
		int n = in.readInt();
		nodes = new ArrayList<FDNode>(n);
		for (int i = 0; i < n; i++) {
			int type = in.readInt();
			String nodename = in.readUTF();
			createNode(type, nodename);
		}
		in.readUTF();
		for (FDNode node: nodes) {
			int pcount = in.readInt();
			for (int i = 0; i<pcount; i++) {
				int pid = in.readInt();
				node.addParentNode(nodes.get(pid));
			}
		}
		dummyRoot = nodes.get(0);
	}
	
	private void writeObject(java.io.ObjectOutputStream out) throws IOException {
		out.writeUTF(name);
		out.writeLong(timePropagation);
		out.writeInt(nodes.size());
		for (FDNode node: nodes) {
			out.writeInt(node.getNodetype());
			out.writeUTF(node.getName());
		}
		out.writeUTF("relation");
		for (FDNode node: nodes) {
			out.writeInt(node.getParentNode().size());
			for (FDNode parent: node.getParentNode())
				out.writeInt(parent.getNodeID());
		}
	}
	
	private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
		 name = in.readUTF();
		 timePropagation = in.readLong();
		 int n = in.readInt();
		 nodes = new ArrayList<FDNode>(n);
		 for (int i = 0; i < n; i++) {
			 int type = in.readInt();
			 String nodename = in.readUTF();
			 createNode(type, nodename);
		 }
		 in.readUTF();
		 for (FDNode node: nodes) {
			 int pcount = in.readInt();
			 for (int i = 0; i<pcount; i++) {
				 int pid = in.readInt();
				 node.addParentNode(nodes.get(pid));
			 }
		 }
		 dummyRoot = nodes.get(0);
	}

	
	public String getName() {
		return this.name;
	}
	
	public FDNode createNode(int nodetype, String name) {
		FDNode node = new FDNode(this, nodetype, name);
		node.setNodeID(nodes.size());
		nodes.add(node);
		if (isAutoConnectToRoot()) node.addParentNode(dummyRoot);
		return node;
	}
	
	public FDNode getDummyRoot() {
		return dummyRoot;
	}
		
	public long propagateDAT() {
		Queue<FDNode> Q = new LinkedBlockingQueue<FDNode>();
		long timeBegin = System.currentTimeMillis(); //System.nanoTime();
		initializeDAT(Q);
		timeInitialization = System.currentTimeMillis() - timeBegin; //  System.nanoTime() - timeBegin;
		
		//int processedNode = 0;
		//int N = nodes.size();
		
		
		while (!Q.isEmpty()) {
			FDNode x = Q.poll();
			if (ready.get(x) == 0) { // check whether the node is ready, but not visited
				//System.out.printf("Visit node %s\n", x.getName());
				ready.put(x, -1);
				int childIdx = 0;
				//long nodestart = System.currentTimeMillis();
				for(FDNode child: x.getChildNode())  {// for each incoming node
					switch (x.getNodetype()) {
					case FDNode.FDNODE_REQUIREMENT:
						x.setDAT(DAT.concat(x.getDAT(), child.getDAT()));
						break;
					case FDNode.FDNODE_COMPOUND:
						x.setDAT(DAT.join(x.getDAT(), child.getDAT()));
						break;
					case FDNode.FDNODE_OBSERVABLE:
						x.setDAT(DAT.concat(x.getDAT(), child.getDAT().applyProbability(x.getChildProbability(child), String.format("%s:%d", x.getName(), childIdx))));
						break;
					}
					childIdx++;
				}
				for (FDNode parent: x.getParentNode()) {
					int r = ready.get(parent) - 1;
					ready.put(parent, r);
					if (r==0)
						Q.add(parent);
				}
				
				//processedNode++;
				//System.out.printf("%d%% (%d/%d) completed. Last processed time: %d\r", 
				//		processedNode*100/N, processedNode, N, (System.currentTimeMillis() - nodestart));
			}
		}
		//System.out.print("                                                                         \r");
		timePropagation = System.currentTimeMillis() - timeBegin - timeInitialization;
		return timePropagation;
	}
	
	public void initializeDAT(Queue<FDNode> Q) {
		Q.clear();
		ready = new HashMap<FDNode, Integer>();
		if (leaves == null)
			leaves = new ArrayList<FDNode>();
		leaves.clear();
		for(FDNode node: getNodes()) 
			if (node.isLeaf()) {
				node.setLeafID(leaves.size());
				leaves.add(node);
			}
			else {
				node.setDAT(null);
				ready.put(node, node.getChildNode().size());
				node.setLeafID(-1);
			}
		for (FDNode node: leaves) {
			DAT dat = new DAT();
			dat.addAlternative(node, 1, 0);
			node.setDAT(dat);
			ready.put(node, 0);
			Q.add(node);
		}
	}
	
	public List<FDNode> getNodes() {
		return nodes;
	}
	
	public long getPropagationTime() {
		return timePropagation;
	}
	
	public float getAverageDegree() {
		int total = dummyRoot.getChildNode().size();
		for (FDNode node: nodes)
			total += node.getChildNode().size() + node.getParentNode().size();
		return ((float)total)/(1 + nodes.size());
	}

	public boolean isAutoConnectToRoot() {
		return autoConnectToRoot;
	}

	public void setAutoConnectToRoot(boolean autoConnectToRoot) {
		this.autoConnectToRoot = autoConnectToRoot;
	}

	public int getLeafCount() {
		return leaves.size();
	}

	public List<FDNode> getLeaves() {
		return leaves;
	}

}
